package com.example.demo.util;

import cn.hutool.core.io.IoUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.example.demo.exception.BizException;
import com.example.demo.exception.ErrorConstants;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;

/**
 * 签名工具类（Java 8兼容版）：生成签名源字符串并提供参数验证能力
 * 核心逻辑：客户端SM2加密签名 → 服务端SM2解密 → 验证SM3哈希一致性
 *
 * @author hao.chen
 * @date 2023/11/6 10:12
 */
@Slf4j
public class SignUtil {

    // ========================= 常量定义（清晰化命名+注释） =========================
    /** 签名有效时长：5分钟（单位：毫秒） */
    private static final long SIGN_VALID_DURATION_MS = 5 * 60 * 1000L;
    /** 对象层级分隔符（前后端必须一致，如"user.name"） */
    private static final String OBJECT_LEVEL_SEPARATOR = ".";
    /** 数组索引格式（如"hobbies[0]"） */
    private static final String ARRAY_INDEX_FORMAT = "%s[%d]";
    /** 需排除的签名参数（签名本身不参与签名计算） */
    private static final String EXCLUDE_PARAM_KEY = "sign";
    /** 默认字符集（UTF-8） */
    private static final String DEFAULT_CHARSET = StandardCharsets.UTF_8.name();


    // ========================= 密钥管理（安全+可维护性优化） =========================
    /**
     * 客户端对应的SM2解密私钥（生产环境建议从配置中心/加密文件加载，避免硬编码）
     * 采用不可修改Map防止运行时被篡改
     */
    private static final Map<String, String> CLIENT_SM2_PRIVATE_KEYS;

    static {
        // Java 8兼容初始化：客户端类型→SM2私钥映射
        Map<String, String> keyMap = new HashMap<>(3);
        keyMap.put("android", "kT6845T0SkSL6qM7NsqIq1KyjTDWJOIP");
        keyMap.put("ios", "SCAmEwggJdAgEAAoGBAKdtss");
        keyMap.put("web", "WZLq3yug3mHnAD4DTS");
        // 禁止修改
        CLIENT_SM2_PRIVATE_KEYS = Collections.unmodifiableMap(keyMap);
    }


    // ========================= 核心验证方法（流程化拆分） =========================

    /**
     * 验证请求签名是否有效
     *
     * @param request HTTP请求（需提前包装为ContentCachingRequestWrapper以支持重复读）
     */
    public static void verifySign(HttpServletRequest request) {
        // 1. 提取并验证签名必需要素（clientType/timestamp等）
        SignElements elements = extractAndValidateSignElements(request);
        // 2. 获取客户端对应的SM2解密私钥
        String sm2PrivateKey = getSm2PrivateKey(elements.getClientType());
        // 3. 解密客户端签名（SM2非对称解密）
        String decryptedSign = decryptSign(sm2PrivateKey, elements.getSign());
        // 4. 生成服务端原始签名串（基于请求参数+固定要素）
        String serverRawSign = generateServerRawSign(elements, request);
        // 5. 验证SM3哈希一致性（核心校验）
        verifySm3Hash(serverRawSign, decryptedSign);
    }


    // ========================= 签名要素处理（逻辑精简） =========================

    /**
     * 提取并验证签名要素（clientType/timestamp/nonce等）
     */
    private static SignElements extractAndValidateSignElements(HttpServletRequest request) {
        SignElements elements = extractSignElements(request);
        validateSignElements(elements);
        return elements;
    }

    /**
     * 从请求Header提取签名要素
     */
    private static SignElements extractSignElements(HttpServletRequest request) {
        return new SignElements(
                request.getRequestURI(),
                getRequiredHeader(request, "clientType", "客户端类型"),
                getRequiredHeader(request, "timestamp", "时间戳"),
                parseTimestamp(getRequiredHeader(request, "timestamp", "时间戳")),
                getRequiredHeader(request, "nonce", "随机数"),
                getRequiredHeader(request, "sign", "签名值")
        );
    }

    /**
     * 验证签名要素合法性（非空/格式/有效期）
     */
    private static void validateSignElements(SignElements elements) {
        // 基础非空校验
        validateNotEmpty(elements.getUri(), "请求URI");
        validateNotEmpty(elements.getClientType(), "客户端类型");
        validateNotEmpty(elements.getNonce(), "随机数");
        validateNotEmpty(elements.getSign(), "签名值");
        // 时间戳格式+有效期校验
        validateTimestamp(elements.getTimestampNum(), elements.getTimestamp());
        validateTimestampValidity(elements.getTimestampNum());
    }


    // ========================= 参数处理（性能+安全性优化） =========================

    /**
     * 构建请求参数字符串（支持JSON/表单格式）
     *
     * @param request 包装后的请求（ContentCachingRequestWrapper，支持重复读）
     */
    public static String buildRequestParamsString(HttpServletRequest request) {
        SortedMap<String, List<String>> flatParams = new TreeMap<>();
        String contentType = request.getContentType();

        if (isJsonContentType(contentType)) {
            // 处理JSON参数（application/json）
            handleJsonParams(request, flatParams);
        } else {
            // 处理表单参数（application/x-www-form-urlencoded）
            handleFormParams(request, flatParams);
        }

        return buildFlatParamsString(flatParams);
    }

    /**
     * 处理JSON格式参数（扁平化JSON为key=value结构）
     */
    private static void handleJsonParams(HttpServletRequest request, SortedMap<String, List<String>> flatParams) {
        // 从缓存获取请求体字节数组（避免直接读流导致的流耗尽问题）
        if (!(request instanceof RepeatedlyRequestWrapper)) {
            return;
        }
        RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
        String jsonBody = IoUtil.read(repeatedlyRequest.getInputStream(), StandardCharsets.UTF_8);
        if (jsonBody == null || jsonBody.isEmpty()) {
            log.debug("JSON请求体为空，不参与签名计算");
            return;
        }
        try {
            if (jsonBody.trim().startsWith("{")) {
                flattenJson("", JSON.parseObject(jsonBody), flatParams);
            } else if (jsonBody.trim().startsWith("[")) {
                flattenJson("", JSON.parseArray(jsonBody), flatParams);
            } else {
                log.warn("不支持的JSON格式（非对象/数组）：{}", jsonBody);
            }
        } catch (Exception e) {
            log.error("解析JSON参数失败：{}", jsonBody, e);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR, "JSON参数格式错误");
        }
    }

    /**
     * 处理表单格式参数（application/x-www-form-urlencoded）
     */
    private static void handleFormParams(HttpServletRequest request, SortedMap<String, List<String>> flatParams) {
        // 遍历表单参数（排除sign参数）
        request.getParameterMap().forEach((key, values) -> {
            if (EXCLUDE_PARAM_KEY.equalsIgnoreCase(key)) {
                log.debug("排除表单参数（不参与签名）：{}", key);
                return;
            }
            // 收集参数值（去空处理）
            for (String value : values) {
                if (StringUtils.hasText(value)) {
                    addFlatParam(flatParams, key, value.trim());
                }
            }
        });
    }


    // ========================= 工具方法（复用性提升） =========================

    /**
     * 获取请求Header中的必填参数
     *
     * @param headerName Header名称
     * @param desc       参数描述（用于异常提示）
     */
    private static String getRequiredHeader(HttpServletRequest request, String headerName, String desc) {
        String value = request.getHeader(headerName);
        if (!StringUtils.hasText(value)) {
            log.error("签名要素缺失：{}（Header参数：{}）", desc, headerName);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR);
        }
        return value.trim();
    }

    /**
     * 解析时间戳（转换为long类型）
     */
    private static long parseTimestamp(String timestampStr) {
        try {
            return Long.parseLong(timestampStr);
        } catch (NumberFormatException e) {
            log.error("时间戳格式错误（必须为数字）：{}", timestampStr, e);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR, "时间戳格式错误");
        }
    }

    /**
     * 验证时间戳有效期（5分钟内）
     */
    private static void validateTimestampValidity(long timestamp) {
        long timeDiff = Math.abs(System.currentTimeMillis() - timestamp);
        if (timeDiff > SIGN_VALID_DURATION_MS) {
            log.error("时间戳过期（超出5分钟）：当前时间={}，请求时间戳={}，差值={}ms",
                    System.currentTimeMillis(), timestamp, timeDiff);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR, "时间戳已过期");
        }
    }

    /**
     * 获取客户端对应的SM2私钥
     */
    private static String getSm2PrivateKey(String clientType) {
        String privateKey = CLIENT_SM2_PRIVATE_KEYS.get(clientType);
        if (!StringUtils.hasText(privateKey)) {
            log.error("不支持的客户端类型（无对应私钥）：{}", clientType);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR, "不支持的客户端类型");
        }
        return privateKey;
    }

    /**
     * 解密客户端签名（SM2非对称解密）
     */
    private static String decryptSign(String sm2PrivateKey, String encryptedSign) {
        try {
            return CryptoUtil.sm2Decrypt(sm2PrivateKey, encryptedSign);
        } catch (Exception e) {
            log.error("SM2解密签名失败（私钥：{}，签名：{}）", sm2PrivateKey, encryptedSign, e);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR, "签名解密失败");
        }
    }

    /**
     * 生成服务端原始签名串（参数串+固定要素）
     */
    private static String generateServerRawSign(SignElements elements, HttpServletRequest request) {
        // 1. 构建请求参数串（JSON/表单参数）
        String paramsStr = buildRequestParamsString(request);
        // 2. 拼接固定要素（uri+clientType+timestamp+nonce+params）
        return String.format("%s&clientType=%s×tamp=%s&nonce=%s&%s",
                elements.getUri(),
                elements.getClientType(),
                elements.getTimestamp(),
                elements.getNonce(),
                paramsStr
        );
    }

    /**
     * 验证SM3哈希一致性
     */
    private static void verifySm3Hash(String serverRawSign, String clientSign) {
        if (!CryptoUtil.sm3VerifyHash(serverRawSign, clientSign)) {
            log.error("SM3哈希校验失败（服务端串：{}，客户端签名：{}）", serverRawSign, clientSign);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR, "签名校验失败");
        }
    }

    /**
     * 判断是否为JSON类型Content-Type
     */
    private static boolean isJsonContentType(String contentType) {
        return contentType != null && contentType.contains("application/json");
    }

    /**
     * 非空校验
     */
    private static void validateNotEmpty(String value, String paramDesc) {
        if (!StringUtils.hasText(value)) {
            log.error("签名要素缺失：{}", paramDesc);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR);
        }
    }

    /**
     * 时间戳有效性校验（必须为正数）
     */
    private static void validateTimestamp(long timestamp, String originalStr) {
        if (timestamp <= 0) {
            log.error("时间戳无效（必须为正数）：{}", originalStr);
            throw new BizException(ErrorConstants.CLIENT_SING_PARAM_ERROR, "时间戳无效");
        }
    }


    // ========================= JSON扁平化与参数拼接（可读性优化） =========================

    /**
     * 递归扁平化JSON（将嵌套结构转为key=value，如{"user":{"name":"a"}}→user.name=a）
     *
     * @param parentKey  父级key（空表示根节点）
     * @param jsonValue  JSON值（JSONObject/JSONArray/基本类型）
     * @param flatParams 扁平化结果Map
     */
    private static void flattenJson(String parentKey, Object jsonValue, SortedMap<String, List<String>> flatParams) {
        if (jsonValue == null) {
            log.debug("JSON扁平化：忽略空值（父key：{}）", parentKey);
            return;
        }

        if (jsonValue instanceof JSONObject) {
            // 处理JSON对象（递归子节点）
            ((JSONObject) jsonValue).forEach((key, value) -> {
                if (EXCLUDE_PARAM_KEY.equalsIgnoreCase(key)) {
                    log.debug("排除JSON参数（不参与签名）：{}", key);
                    return;
                }
                String currentKey = buildObjectKey(parentKey, key);
                flattenJson(currentKey, value, flatParams);
            });
        } else if (jsonValue instanceof JSONArray) {
            // 处理JSON数组（带索引，如hobbies[0]）
            JSONArray array = (JSONArray) jsonValue;
            for (int i = 0; i < array.size(); i++) {
                String currentKey = buildArrayKey(parentKey, i);
                flattenJson(currentKey, array.get(i), flatParams);
            }
        } else {
            // 处理基本类型（字符串/数字/布尔等）
            String value = jsonValue.toString().trim();
            if (StringUtils.hasText(value)) {
                addFlatParam(flatParams, parentKey, value);
            }
        }
    }

    /**
     * 构建对象类型key（如"user.name"）
     */
    private static String buildObjectKey(String parentKey, String key) {
        return parentKey.isEmpty() ? key : parentKey + OBJECT_LEVEL_SEPARATOR + key;
    }

    /**
     * 构建数组类型key（如"hobbies[0]"）
     */
    private static String buildArrayKey(String parentKey, int index) {
        return parentKey.isEmpty() ?
                // 根数组："[0]"
                String.format(ARRAY_INDEX_FORMAT, "", index) :
                // 嵌套数组："user.hobbies[0]"
                String.format(ARRAY_INDEX_FORMAT, parentKey, index);
    }

    /**
     * 添加参数到扁平化Map（去重+排序）
     */
    private static void addFlatParam(SortedMap<String, List<String>> flatParams, String key, String value) {
        flatParams.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
        // 降级为trace级别，减少日志噪音
        log.trace("添加签名参数：{}={}", key, value);
    }

    /**
     * 生成参数字符串（key=value&key2=value2，UTF-8编码）
     */
    private static String buildFlatParamsString(SortedMap<String, List<String>> flatParams) {
        return flatParams.entrySet().stream()
                .flatMap(entry -> entry.getValue().stream()
                        .map(value -> urlEncode(entry.getKey()) + "=" + urlEncode(value))
                )
                .collect(Collectors.joining("&"));
    }

    /**
     * URL编码（UTF-8，空格转为%20而非+）
     */
    private static String urlEncode(String str) {
        try {
            return URLEncoder.encode(str, DEFAULT_CHARSET).replace("+", "%20");
        } catch (Exception e) {
            log.error("URL编码失败（字符串：{}）", str, e);
            throw new BizException(ErrorConstants.SERVER_INTERNAL_ERROR, "参数编码失败");
        }
    }


    // ========================= 签名要素封装类（数据载体） =========================
    @Getter
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    private static class SignElements {
        private final String uri;           // 请求URI
        private final String clientType;    // 客户端类型（android/ios/web）
        private final String timestamp;     // 时间戳字符串
        private final long timestampNum;    // 时间戳数字值
        private final String nonce;         // 防重放随机数
        private final String sign;          // 客户端加密签名
    }
}
